// ----------------------------------------------------------------------------------
// Microsoft Developer & Platform Evangelism
// 
// Copyright (c) Microsoft Corporation. All rights reserved.
// 
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 
// EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 
// OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
// ----------------------------------------------------------------------------------
// The example companies, organizations, products, domain names,
// e-mail addresses, logos, people, places, and events depicted
// herein are fictitious.  No association with any real company,
// organization, product, domain name, email address, logo, person,
// places, or events is intended or should be inferred.
// ----------------------------------------------------------------------------------

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Windows;
using SL.IdentityModel.Claims;
using SL.IdentityModel.Protocols.WSTrust;

namespace SL.IdentityModel.Services
{
    /// <summary>
    /// Class manages the ClaimsIdentitySession
    /// can set session cookie given an issued token, sign out of session, get session information from server to populate the 
    /// user idenity information.
    /// </summary>
    public class ClaimsIdentitySessionManager : IApplicationService, INotifyPropertyChanged
    {
        public delegate UsernameCredentials UsernameRequiredEventHandler( object sender, string endpoint );

        private AuthenticationServiceClient _authenticationServiceClient;
        private static ClaimsIdentitySessionManager _current = null;
        private ISecurityTokenService _identityProvider = null;
        private static object _lock = new object();
        private TokenCache _tokenCache = new TokenCache();
        private IClaimsPrincipal _user = null;

        public event EventHandler<ClaimsIdentityEventArgs> GetClaimsIdentityComplete;
        public event EventHandler<SignOutEventArgs> SignOutComplete;
        public event EventHandler<InvokeAsyncCompleteEventArgs> InvokeAsyncComplete;
        public event EventHandler<SignInEventArgs> SignInComplete;
        public event PropertyChangedEventHandler PropertyChanged;
        public event UsernameRequiredEventHandler UsernameRequired;

        public Uri AuthenticationServiceEndPoint { get; set; }

        public string ApplicationIdentifier { get; set; }

        /// <summary>
        /// gets current session manager for current session information and management 
        /// </summary>
        public static ClaimsIdentitySessionManager Current
        {
            get
            {
                if ( _current == null )
                {
                    lock ( _lock )
                    {
                        if ( _current == null )
                        {
                            _current = new ClaimsIdentitySessionManager();
                        }
                    }
                }
                return _current;
            }
        }

        /// <summary>
        /// Configures identity provider security token service
        /// </summary>
        public ISecurityTokenService IdentityProvider
        {
            get
            {
                return _identityProvider;
            }
            set
            {
                _identityProvider = value;
            }
        }

        /// <summary>
        /// Current user information
        /// </summary>
        public IClaimsPrincipal User
        {
            get { return _user; }

            private set
            {
                _user = value;
                NotifyPropertyChanged( "User" );
            }
        }

        /// <summary>
        /// Creates the Current Claims session manager
        /// This is public to allow class to be created from app.xaml
        /// sets default path to AuthenticationServie.svc
        /// </summary>
        public ClaimsIdentitySessionManager()
        {
            if (null == _current)
            {
                this.AuthenticationServiceEndPoint = new Uri(Application.Current.Host.Source, "../AuthenticationService.svc");
                _current = this;
            }
        }

        public void InvokeAsync( Action asyncOperation, IClientChannel innerChannel )
        {
            InvokeAsync( asyncOperation, innerChannel, WSTrust13Constants.KeyTypes.Symmetric );
        }

        /// <summary>
        /// Calls given an Action containing a WCF async proxy call, addeds an issuedToken to message header.
        /// if no token for the service is in the cache, a new one will be aquired from the configures ws-trust identityprovider.
        /// This may result in credentials having to be specified, this can be done using the UsernameRequired delegate
        /// </summary>
        /// <example>
        /// .InvokeAsync( ()=>{ client.doCallAsync(callParam);}, client.innerChannel );
        /// </example>
        /// <param name="asyncOperation">Action calling proxy fuction.</param>
        /// <param name="innerChannel">client proxy innerChannel</param>
        public void InvokeAsync( Action asyncOperation, IClientChannel innerChannel, string keyType )
        {
            IRequestCredentials credentials = null;
            WSTrustBinding binding = null;

            if ( null == asyncOperation )
            {
                throw new ArgumentNullException( "asyncOperation" );
            }

            if ( null == innerChannel )
            {
                throw new ArgumentNullException( "innerChannel" );
            }

            if ( !( this.IdentityProvider is WSTrustSecurityTokenService ) )
            {
                throw new ArgumentException( "IdentityProvider is not a WSTrustSecurityTokenService" );
            }

            string serviceAppliesTo = innerChannel.Via.ToString();

            if ( _tokenCache.HasTokenInCache( serviceAppliesTo ) )
            {
                DoServiceCall( serviceAppliesTo, asyncOperation, innerChannel );
            }
            else
            {
                WSTrustSecurityTokenService sts = this.IdentityProvider as WSTrustSecurityTokenService;

                switch ( sts.CredentialType )
                {
                    case WSTrustBindingCredentialType.Username:

                        if ( null == UsernameRequired )
                        {
                            throw new ArgumentNullException( "UsernameRequired" );
                        }
                        credentials = UsernameRequired( this, sts.Endpoint );
                        binding = new WSTrustBindingUsernameMixed();
                        break;
                    case WSTrustBindingCredentialType.DefaultCredential:
                        credentials = new WindowsCredentials();
                        binding = new WSTrustBindingWindows();
                        break;
                    default:
                        throw new ArgumentException( "WSTrustBindingType not configurable" );
                }

                EndpointAddress stsEndpoint = new EndpointAddress( sts.Endpoint );
                WSTrustClient trustClient = new WSTrustClient( binding, stsEndpoint, credentials );
                trustClient.IssueCompleted += new EventHandler<IssueCompletedEventArgs>( trustClient_MakeServiceCall );
                RequestSecurityToken rst = new RequestSecurityToken( keyType );
                rst.AppliesTo = new EndpointAddress( serviceAppliesTo );

                trustClient.IssueAsync( rst, new ServiceCallState( asyncOperation, innerChannel, serviceAppliesTo ) );
            }
        }

        /// <summary>
        /// Signs into current session with username password, to authenticate to a ws-trust sts.
        /// this then sets a session cookie that can be used to call cookie protected services
        /// </summary>
        /// <param name="username"></param>
        /// <param name="password"></param>
        public void SignInUsernameAsync( string username, string password )
        {
            if ( !( this.IdentityProvider is WSTrustSecurityTokenService ) ||
                ( WSTrustBindingCredentialType.Username != ( this.IdentityProvider as WSTrustSecurityTokenService ).CredentialType ) )
            {
                throw new Exception( "WSTrustSecurityTokenService with username binding is not configured" );
            }

            GetToken( ( this.IdentityProvider as WSTrustSecurityTokenService ).Endpoint,
                this.ApplicationIdentifier,
                new WSTrustBindingUsernameMixed(),
                new UsernameCredentials( username, password ) );
        }

        /// <summary>
        /// Signs into current session with current login session
        /// </summary>
        /// <param name="username"></param>
        /// <param name="password"></param>
        public void SignInDefaultCredentials()
        {
            if ( !( this.IdentityProvider is WSTrustSecurityTokenService ) ||
               ( WSTrustBindingCredentialType.DefaultCredential != ( this.IdentityProvider as WSTrustSecurityTokenService ).CredentialType ) )
            {
                throw new Exception( "WSTrustSecurityTokenService with DefaultCredential is not configured" );
            }

            GetToken( ( this.IdentityProvider as WSTrustSecurityTokenService ).Endpoint,
                this.ApplicationIdentifier,
                new WSTrustBindingWindows(),
                new WindowsCredentials() );
        }

        /// <summary>
        /// Signs out user by clearing session cookie
        /// </summary>
        public void SignOutAsync()
        {
            _authenticationServiceClient.SignOutAsync();
        }

        /// <summary>
        /// Gets users claims information from current session, and populates User field
        /// </summary>
        public void GetClaimsIdentityAsync()
        {
            _authenticationServiceClient.SignInAsync();
        }

        /// <summary>
        /// Given a issued token, sets session cookie by calling AuthenticationService.svc
        /// </summary>
        /// <param name="issuedToken"></param>
        public void SetSessionCookieAsync( string issuedToken )
        {
            _authenticationServiceClient.SignInWithIssuedTokenAsync( issuedToken );
        }

        /// <summary>
        /// called onse session cookie is set, using issued token
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void AuthenticationServiceClient_SignInWithIssuedTokenCompleted( object sender, SignInWithIssuedTokenCompletedEventArgs e )
        {
            if (null != e.Error)
            {
                this.User = null;
            }
            else
            {
                this.User = CreateIClaimsPrincipal(e.Result);
            }

            if (null != SignInComplete)
            {
                SignInComplete(sender, new SignInEventArgs(User, e.Error));
            }
        }

        /// <summary>
        /// calls a WCF service with an issuedToken binding, placing the issuedToken in the message header
        /// </summary>
        /// <param name="serviceAppliesTo"></param>
        /// <param name="asyncOperation"></param>
        /// <param name="contextChannel"></param>
        private void DoServiceCall( string serviceAppliesTo, Action asyncOperation, IContextChannel contextChannel )
        {
            using ( OperationContextScope scope = new OperationContextScope( contextChannel ) )
            {
                IssuedTokenHeader header = new IssuedTokenHeader( _tokenCache.GetTokenFromCache( serviceAppliesTo ) );
                OperationContext.Current.OutgoingMessageHeaders.Add( header );
                asyncOperation();
            }
        }

        /// <summary>
        /// Calls ws-trust security token service to request a token
        /// </summary>
        /// <param name="stsEndpoint">security token service endpoint url</param>
        /// <param name="appliesTo">appliesTo identifing target which the token is intended for</param>
        /// <param name="binding">WCF binding to use for the call</param>
        /// <param name="credentials">credentials to use to authenticate to the security token service</param>
        private void GetToken( string stsEndpoint, string appliesTo, Binding binding, IRequestCredentials credentials )
        {
            if ( null == stsEndpoint )
            {
                throw new ArgumentNullException( "stsEndpoint" );
            }

            if ( null == appliesTo )
            {
                throw new ArgumentNullException( "appliesTo" );
            }

            if ( null == binding )
            {
                throw new ArgumentNullException( "binding" );
            }

            if ( null == credentials )
            {
                throw new ArgumentNullException( "credentials" );
            }

            WSTrustClient trustClient = new WSTrustClient( binding, new EndpointAddress( stsEndpoint ), credentials );

            trustClient.IssueCompleted += new EventHandler<IssueCompletedEventArgs>( trustClient_IssueCompleted );

            RequestSecurityToken rst = new RequestSecurityToken(WSTrust13Constants.KeyTypes.Bearer);
            rst.AppliesTo = new EndpointAddress( appliesTo );

            trustClient.IssueAsync( rst, new ServiceCallState( null, null, appliesTo ) );
        }

        /// <summary>
        /// Called on return of GetClaimsIdentity async call
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void AuthenticationServiceClient_GetClaimsIdentityComplete( object sender, SignInCompletedEventArgs e )
        {

            if ( null != e.Error )
            {
                this.User = null;
            }
            else
            {
                this.User = CreateIClaimsPrincipal( e.Result );
            }

            if ( null != this.GetClaimsIdentityComplete )
            {
                this.GetClaimsIdentityComplete( sender, new ClaimsIdentityEventArgs( this.User, e.Error ) );
            }
        }

        /// <summary>
        /// given claims collection from AthenticationService.svc, creates a ClaimsPrincipal
        /// </summary>
        /// <param name="claims"></param>
        /// <returns></returns>
        private IClaimsPrincipal CreateIClaimsPrincipal( ObservableCollection<AuthenticationServiceIdentityClaim> claims )
        {
            IClaimsIdentity identity = new ClaimsIdentity();

            foreach ( AuthenticationServiceIdentityClaim c in claims )
            {
                identity.Claims.Add( new Claim( c.Issuer, c.OriginalIssuer, c.Type, c.Value, c.ValueType ) );
            }

            return new ClaimsPrincipal( identity );
        }

        /// <summary>
        /// returns after async call to SignOut user is completed
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void AuthenticationServiceClient_SignOutCompleted( object sender, AsyncCompletedEventArgs e )
        {
            if ( null == e.Error )
            {
                this.User = new ClaimsPrincipal( new ClaimsIdentity() );
            }

            if ( null != this.SignOutComplete )
            {
                this.SignOutComplete( sender, new SignOutEventArgs( e.Error ) );
            }
        }

        /// <summary>
        /// /// returns after async call to Issue is completed
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void trustClient_IssueCompleted( object sender, IssueCompletedEventArgs e )
        {
            if ( null == e.Error )
            {
                ClaimsIdentitySessionManager.Current.SetSessionCookieAsync( e.Result.RequestedSecurityToken.RawToken );
            }
            else if ( null != SignInComplete )
            {
                SignInComplete( sender, new SignInEventArgs( null, e.Error ) );
            }
        }

        private void NotifyPropertyChanged( String info )
        {
            if ( PropertyChanged != null )
            {
                PropertyChanged( this, new PropertyChangedEventArgs( info ) );
            }
        }

        /// <summary>
        /// called after a ws-trust call is made to get a issued token for the service call
        /// the service is then called using the token to authenticate
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void trustClient_MakeServiceCall( object sender, IssueCompletedEventArgs e )
        {
            if ( null == e.Error )
            {
                RequestSecurityTokenResponse rstr = e.Result;
                string appliesTo;

                if ( null == rstr.AppliesTo || null == rstr.AppliesTo.Uri )
                {
                    throw new ArgumentException( "No appliesTo in RequestSecurityTokenResponse" );
                }

                appliesTo = rstr.AppliesTo.Uri.AbsoluteUri;
                _tokenCache.AddTokenToCache( rstr.AppliesTo.Uri.AbsoluteUri, rstr );
                ServiceCallState state = e.UserState as ServiceCallState;
                DoServiceCall( appliesTo, state.Action, state.Channel );
            }
            else
            {
                if ( null != InvokeAsyncComplete )
                {
                    InvokeAsyncComplete( sender, new InvokeAsyncCompleteEventArgs( e.Error ) );
                }
            }
        }

        #region IApplicationService

        public void StartService( ApplicationServiceContext context )
        {
            Application.Current.Resources.Add( "ClaimsIdentitySessionManager", Current );

            _authenticationServiceClient = new AuthenticationServiceClient(
                    new CustomBinding(
                        new BinaryMessageEncodingBindingElement(),
                        new HttpsTransportBindingElement()
                        ), new EndpointAddress( this.AuthenticationServiceEndPoint ) );

            _authenticationServiceClient.SignInCompleted += new EventHandler<SignInCompletedEventArgs>(AuthenticationServiceClient_GetClaimsIdentityComplete);
            _authenticationServiceClient.SignInWithIssuedTokenCompleted += new EventHandler<SignInWithIssuedTokenCompletedEventArgs>(AuthenticationServiceClient_SignInWithIssuedTokenCompleted);
            _authenticationServiceClient.SignOutCompleted += new EventHandler<AsyncCompletedEventArgs>(AuthenticationServiceClient_SignOutCompleted);

            this.User = new ClaimsPrincipal( new ClaimsIdentity() );

            if ( Current.IdentityProvider is WSFederationSecurityTokenService )
            {
                this.GetClaimsIdentityAsync();
            }
        }

        public void StopService()
        {
            _authenticationServiceClient.SignInCompleted -= new EventHandler<SignInCompletedEventArgs>(AuthenticationServiceClient_GetClaimsIdentityComplete);
            _authenticationServiceClient.SignInWithIssuedTokenCompleted -= new EventHandler<SignInWithIssuedTokenCompletedEventArgs>(AuthenticationServiceClient_SignInWithIssuedTokenCompleted);
            _authenticationServiceClient.SignOutCompleted -= new EventHandler<AsyncCompletedEventArgs>(AuthenticationServiceClient_SignOutCompleted);
        }

        #endregion
    }
}